package com.halcyon.di;

import android.util.Log;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;

import static java.lang.String.format;

/**
 * Given a DependencyInjectingObjectFactory, injects dependencies into a class.
 */
public class DependencyInjector {
    private DependencyInjectingObjectFactory dependencyInjectingObjectFactory;

    public DependencyInjector(DependencyInjectingObjectFactory aDependencyInjectingObjectFactory) {
        dependencyInjectingObjectFactory = aDependencyInjectingObjectFactory;
    }

    @SuppressWarnings("unchecked")
    public <T> void injectDependenciesForClassHierarchy(T anObject) {
        // the reflection API requires that each class in the hierarchy be considered
        // start with the lowest class in the hierarchy
        Class<?> interfaceOfObject = anObject.getClass();

        do {
            // inject the dependencies for this class
            injectDependenciesForSingleClass(anObject, (Class<T>) interfaceOfObject);

            // move on up the class hierarchy...
            interfaceOfObject = interfaceOfObject.getSuperclass();

            // until the top is reached
        } while (interfaceOfObject != null);
    }

    private <T, S extends T> void injectDependenciesForSingleClass(S anObject, Class<T> aClass) {
//		Log.d("DI", "inject for " + aClass.getName());
        // for each
        for (Field field : aClass.getDeclaredFields()) {
            if (field.isAnnotationPresent(Dependency.class)) {
//				Log.d("DI", "inject for field " + field.getName());
                field.setAccessible(true);
                try {
//					Log.d("DI", "before inject");
                    // if the field has not already been set (possibly through injection)...
                    if (field.get(anObject) == null) {
                        Class<?> classOfDependency = field.getType();
                        Object injectedValue;
                        final int modifiers = classOfDependency.getModifiers();
                        if (Modifier.isInterface(modifiers) || Modifier.isAbstract(modifiers)) {
//							Log.d("DI", "INTERFACE CREATING PROXY " + classOfDependency.getName());
                            try {
                                injectedValue = dependencyInjectingObjectFactory.getObject(classOfDependency);
//								Log.d("DI", "ALREADY IN DI FACTORY " + injectedValue);
                            } catch (RuntimeException e) {
//								Log.e("DI", "NO REGISTERED OBJECT");
                                injectedValue = createProxy(classOfDependency);
                            }
                        } else {
                            injectedValue = dependencyInjectingObjectFactory.getObject(classOfDependency);

//							Log.d("DI", "INJECT WITH NONE INTERFACE" + injectedValue);
                        }
                        field.set(anObject, injectedValue);
                    } else {
                        Log.e("DI", "THE FIELD IS NOT NULL");
                    }
                } catch (IllegalArgumentException e) {
                    throw new RuntimeException(e);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(format("Unable to access field %s.", field.getName()), e);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private <T> T createProxy(final Class<T> aClass) {
        return (T) Proxy.newProxyInstance(DependencyInjectingObjectFactory.class.getClassLoader(),
                new Class[]{aClass}, new InvocationHandler() {
                    private T object;

                    public Object invoke(Object aProxy, Method aMethod, Object[] anArrayOfArguments) throws Throwable {
                        if (object == null) {
                            final ObjectSource<T> objectSource = dependencyInjectingObjectFactory
                                    .getObjectSource(aClass);
                            if (objectSource == null) {
                                throw new RuntimeException(format(
                                        "No source was registered for the dependecy of type %s.", aClass.getName()));
                            }
                            object = (T) objectSource.getObject();
                        }
                        return aMethod.invoke(object, anArrayOfArguments);
                    }
                });
    }

}
